%  This file is part of Erlywog.

%  Erlywog is free software: you can redistribute it and/or modify
%  it under the terms of the GNU General Public License as published by
%  the Free Software Foundation, either version 3 of the License, or
%  (at your option) any later version.

%  Erlywog is distributed in the hope that it will be useful,
%  but WITHOUT ANY WARRANTY; without even the implied warranty of
%  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%  GNU General Public License for more details.

%  You should have received a copy of the GNU General Public License
%  along with Erlywog.  If not, see <http://www.gnu.org/licenses/>.

%  Copyright Jasm Sison 2009

-module(key_manager).
-include_lib("eunit/include/eunit.hrl"). %% turn on unit tests
-export([
	start/0
	, stop/0
	, boot/0
	, issue_static_salt/0
	, issue_dynamic_salt/1
	, issue_session_key/1
	, issue_fake_salt/1
	, get_session_key/1
	, get_dynamic_salt/1
	, get_fake_salt/1
	, validate_session_key/2
	, validate_dynamic_salt/2
	, key_population_size/0
	, key_size/0
]).

%% TODO
%% + major broken feature, what happens when there are no salts left? -> fixed, refills when certain threshold met.
%% - issue expiring fake salts
%% + make ETS processes private

% list_process_names() ->
%   [ io:format("~p~n", [N]) || N <- [static_salt_pool, dynamic_salt_pool, session_key_pool, active_session_key_pool]].

start() ->
  P = process_util,
  [
      P:make_process(static_salt_pool, fun create_static_salt_pool/0)
    , P:make_process(dynamic_salt_pool, fun create_dynamic_salt_pool/0)
    , P:make_process(session_key_pool, fun create_session_key_pool/0)
    , P:make_process(active_session_key_pool, fun create_active_session_key_pool/0)
    , P:make_process(active_dynamic_salt_pool, fun create_active_dynamic_salt_pool/0)
    , P:make_process(fake_salt_pool, fun create_fake_salt_pool/0)
  ].

stop() ->
    Names = [
        static_salt_pool
      , dynamic_salt_pool
      , session_key_pool
      , active_session_key_pool
      , active_dynamic_salt_pool
      , fake_salt_pool
    ]
  , [ process_util:kill_process(Name) || Name <- Names ].
  
boot() ->
  start().

%% Constants
key_population_size() -> 10000.
key_size() -> 64.

%% Reusable string generation code
make_keys_bag(TableName, N, Length) ->
    TableId = ets:new(TableName,[bag, private, {keypos, 1}]),
    make_keys_in_set(Length, TableId, N),
    TableId.

sanitize_value(Key) -> base64:encode(Key).

make_keys_in_set(_ , _, 0) -> void;
make_keys_in_set (Length, TableId, N) when N > 0 ->
  ets:insert(TableId, {sanitize_value(generate_random_key(Length))}),
  make_keys_in_set(Length, TableId, N-1).

generate_random_key(N) ->
  {A,B,C} = now(),
  random:seed(A,B,C),
  lists:map(fun (_) -> random:uniform(90)+$\s+1 end, lists:seq(1,N)).
  
%% Create pools
create_static_salt_pool () ->
  TableId = make_keys_bag(static_salt_bag, key_population_size(), key_size()),
  loop(TableId).

create_dynamic_salt_pool () ->
  TableId = make_keys_bag(dynamic_salt_bag, key_population_size(), key_size()),
  loop(TableId).
  
create_session_key_pool () ->
  TableId = make_keys_bag(session_key_bag, key_population_size(), key_size()),
  loop(TableId).
  
create_active_set(Name) ->
  TableId = ets:new(Name, [set, private, {keypos, 1}]),
  loop(TableId).

create_active_session_key_pool () ->
  create_active_set(active_session_key_set).
  
create_active_dynamic_salt_pool () ->
  create_active_set(active_dynamic_salt_set).
  
create_fake_salt_pool () -> 
  create_active_set(fake_salt_set).
  
%% TODO + refill code
%% loops/event handlers
table_size(TableId) ->
  proplists:get_value(size, ets:info(TableId)).

floor(X) ->
  T = trunc(X),
  case X - T < 0 of
    true -> T - 1;
    false -> T
  end.

loop(TableId) ->
  receive
    {From, stop} ->
      io:format("Ets table ~p, received stop signal.~n",[TableId]),
      From ! ets:delete(TableId); % boolean
    {From, extract} ->
      Size = table_size(TableId),
      case Size > floor(0.25 * key_population_size()) of
        false ->
          make_keys_in_set(key_size(), TableId, key_population_size())
        ;_ -> ok
      end
      , First = ets:first(TableId)
      , ets:delete(TableId, First) % boolean
      , From ! First
      , loop(TableId);
    {From, lookup, Key} ->
      From ! proplists:get_value(Key, ets:lookup(TableId, Key), not_found) % atom
      , loop(TableId);
    {From, insert, Key, Value} ->
      From ! ets:insert(TableId, {Key, Value}) % boolean
      , loop(TableId);
    Other ->
      io:format("Received unexpected message: ~p~n", [Other]),
      loop(TableId)
  end.
  
receive_message() ->
  receive
    Other ->
      Other
  end.
  
lookup(Name, Key) ->
  Name ! {self(), lookup, Key},
  receive_message().

extract(Name) ->
  Name ! {self(), extract},
  receive_message().
  
insert(Name, Key, Value) ->
  Name ! {self(), insert, Key, Value},
   receive_message().

%% Stored on the database (persistent)
issue_static_salt() ->
  extract(static_salt_pool).

%% Stored on the process
issue_dynamic_salt(User) ->
  Salt = extract(dynamic_salt_pool),
  insert(active_dynamic_salt_pool, User, Salt),
  Salt.
  
issue_fake_salt(User) ->
  Salt = extract(static_salt_pool),
  insert(fake_salt_pool, User, Salt),
  Salt.
  
get_fake_salt(User) ->
  lookup(fake_salt_pool, User).
  
validate_dynamic_salt(User, Salt) ->
  lookup(active_dynamic_salt_pool, User) =:= Salt.
  
get_dynamic_salt(User) ->
  lookup(active_dynamic_salt_pool, User).

%% Stored on the process
issue_session_key(User) ->
  SessionKey = extract(session_key_pool),
  insert(active_session_key_pool, User, SessionKey),
  SessionKey.
  
validate_session_key(User, Key) ->
  lookup(active_session_key_pool, User) =:= Key.
  
get_session_key(User) ->
  lookup(active_session_key_pool, User).

%% tests!  
session_validation_test_() ->
  SessionKey = extract(session_key_pool), %% get session key
  insert(active_session_key_pool, SessionKey, SessionKey), %% store session key
  Key2 = lookup(active_session_key_pool, SessionKey),
  io:format("~p~n is equal to~n~p~n", [Key2,SessionKey]),
  ?assert(SessionKey =:= Key2).
  
multiple_session_validation_test_() ->
  Names = [joe,moe,bob,bill,dog,god,small,big,cunt,cock,balls,clit,tits,fuck,lick,suck,rim]
  , Keys = lists:map(fun (Name) -> issue_session_key(Name) end, Names)
  , Result = lists:zipwith(fun (Name, Key) -> validate_session_key(Name, Key) end, Names, Keys)
  , ?assert(lists:foldl(fun(T0, T1) -> T0 =:= T1 end, true, Result)).
  
multiple_salt_validation_test_() ->
  Names = [joe,moe,bob,bill,dog,god,small,big,cunt,cock,balls,clit,tits,fuck,lick,suck,rim]
  , Keys = lists:map(fun (Name) -> issue_dynamic_salt(Name) end, Names)
  , Result = lists:zipwith(fun (Name, Key) -> validate_dynamic_salt(Name, Key) end, Names, Keys)
  , ?assert(lists:foldl(fun(T0, T1) -> T0 =:= T1 end, true, Result)).

